package com.guchenbo.example.io;

import cn.hutool.core.util.NumberUtil;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @author guchenbo
 * @date 2022/4/29
 */
public class GuFileSystem {
    protected static final Logger log = LoggerFactory.getLogger(GuFileSystem.class);

    /**
     * 文件大小上限，1KB
     */
    public static final Integer FILE_MAX_SIZE = 1024 * 1024;
    public static final String INDEX_EXT = ".index";
    public static final String DATA_EXT = ".data";
    public static final String NAME_PRE = "gfs-";

    /**
     * 每个索引的长度，固定8个字节，前4个字节表示数据下标，后4个字节表示数据长度，总共两个Integer的长度
     */
    public static final Integer INDEX_LEN_PRE = 8;

    private GuSerializer serializer;
    private GuDeserializer deserializer;

    public GuFileSystem() {
        this(new StringSerializer(), new StringDeserializer());
    }

    public GuFileSystem(GuSerializer serializer, GuDeserializer deserializer) {
        this.serializer = serializer;
        this.deserializer = deserializer;
    }

    /**
     * 文件写入数据
     *
     * @param dir
     * @param datas
     * @throws IOException
     */
    public void writeData(String dir, List<String> datas) throws IOException {
        int fileIdx = getLatestIdx(dir);
        File dataFile = new File(dir, buildFileName(fileIdx, DATA_EXT));
        FileOutputStream dataStream = new FileOutputStream(dataFile, true);
        FileOutputStream indexStream = new FileOutputStream(new File(dir, buildFileName(fileIdx, INDEX_EXT)), true);

        long fileLen = dataFile.length();
        // 文件的起始大小
        int offset = (int) fileLen;
        // 计算当前文件里已经存放里多少数据，从而得出下一个数据起始位置
        int startIdx = calStartDataIdx(dir, fileIdx);

        for (int i = 0; i < datas.size(); i++) {
            byte[] bs = serializer.serialize(datas.get(i));
            int len = bs.length;
            if (offset + len > FILE_MAX_SIZE && offset > 0) {
                // offset > 0 保证第一个能写入，不管多大
                // 满了
                int nextIdx = startIdx + i;
                createNewFs(dir, nextIdx);
                dataStream.flush();
                indexStream.flush();

                dataStream = new FileOutputStream(new File(dir, buildFileName(nextIdx, DATA_EXT)), true);
                indexStream = new FileOutputStream(new File(dir, buildFileName(nextIdx, INDEX_EXT)), true);

                // 重置起始大小
                offset = 0;
            }
            dataStream.write(bs);
            indexStream.write(NumberUtil.toBytes(offset));
            indexStream.write(NumberUtil.toBytes(len));
            // 文件大小增加
            offset += len;
        }
        dataStream.flush();
        indexStream.flush();
    }

    /**
     * 获取第几个数据
     *
     * @param dataPath
     * @param count    第几个数据
     * @return
     */
    public String readData(String dataPath, int count) throws Exception {
        // 转换成下标
        int index = count - 1;
        int fileIdx = findFileIdx(dataPath, index);
        log.info("mesage count {} in file {}", count, fileIdx);
        Preconditions.checkState(fileIdx >= 0, "当前位置数据不存在");
        int diff = index - fileIdx;

        FileInputStream indexStream = new FileInputStream(new File(dataPath, buildFileName(fileIdx, INDEX_EXT)));

        int offset = diff * INDEX_LEN_PRE;
        indexStream.skip(offset);
        byte[] indexByte = new byte[INDEX_LEN_PRE];
        int read = indexStream.read(indexByte);
        Preconditions.checkState(read == INDEX_LEN_PRE, "未读取到完整数据");
        byte[] offsetByte = Arrays.copyOfRange(indexByte, 0, 4);
        byte[] lenByte = Arrays.copyOfRange(indexByte, 4, INDEX_LEN_PRE);

        int dataOffset = NumberUtil.toInt(offsetByte);
        int dataLen = NumberUtil.toInt(lenByte);

        FileInputStream dataStream = new FileInputStream(new File(dataPath, buildFileName(fileIdx, DATA_EXT)));
        byte[] dataByte = new byte[dataLen];
        dataStream.skip(dataOffset);
        read = dataStream.read(dataByte);
        Preconditions.checkState(read == dataLen, "未读取到完整数据");

        indexStream.close();
        dataStream.close();
        return (String) deserializer.deserialize(dataByte);
    }

    /**
     * 计算当前文件里已经存放里多少数据，从而得出下一个数据起始位置，下标
     *
     * @param dir
     * @param fileIdx
     * @return
     */
    private int calStartDataIdx(String dir, int fileIdx) throws IOException {
        FileInputStream indexStream = new FileInputStream(new File(dir, buildFileName(fileIdx, INDEX_EXT)));
        // 读取最后8个字节，得出上次保存到里第几个数据
        int len = indexStream.available();
        if (len > 0) {
            // 有几个数据
            int cnt = len / INDEX_LEN_PRE;
            // 比如起始是3，里面写了4个数据，下一个起始应该是3+4=7
            // 这里都是下标
            return fileIdx + cnt;
        } else {
            return 0;
        }
    }

    /**
     * 根据数据下标获取文件位置
     *
     * @param dataPath
     * @param index    数据下标
     * @return
     */
    private int findFileIdx(String dataPath, int index) {
        List<Integer> sort = listAndSortIndex(dataPath);
        for (int i = 0; i < sort.size(); i++) {
            if (sort.get(i) > index) {
                if (i == 0) {
                    return -1;
                } else {
                    return sort.get(i - 1);
                }
            }
        }
        return sort.get(sort.size() - 1);
    }

    private List<Integer> listAndSortIndex(String dataPath) {
        File[] files = listIndexFile(dataPath);
        return sortIndexFile(Lists.newArrayList(files));
    }

    private String buildFileName(int fileIdx, String ext) {
        return NAME_PRE + fileIdx + ext;
    }

    private Integer getLatestIdx(String path) throws IOException {
        File[] files = listIndexFile(path);
        List<Integer> sort = sortIndexFile(Lists.newArrayList(files));
        Optional<Integer> max = sort.stream().max(Comparator.naturalOrder());
        if (!max.isPresent()) {
            createNewFs(path, 0);
        }
        return max.orElse(0);
    }

    private File[] listIndexFile(String path) {
        File dir = new File(path);
        Preconditions.checkState(dir.isDirectory(), "路径不是目录");
        File[] files = dir.listFiles(((d, name) -> name.startsWith(NAME_PRE) && name.endsWith(INDEX_EXT)));
        return files == null ? new File[0] : files;
    }

    private List<Integer> sortIndexFile(List<File> files) {
        if (files == null) {
            return Lists.newArrayList();
        }
        return files.stream().map(f -> getFileIdx(f.getName())).sorted().collect(Collectors.toList());
    }

    private int getFileIdx(String filename) {
        return Integer.parseInt(StringUtils.substringAfter(StringUtils.substringBefore(filename, INDEX_EXT), NAME_PRE));
    }

    private void createNewFs(String dir, int fileIdx) throws IOException {
        File dataFile = new File(dir, buildFileName(fileIdx, DATA_EXT));
        File indexFile = new File(dir, buildFileName(fileIdx, INDEX_EXT));
        //        System.out.println(dataFile.exists());
        //        System.out.println(indexFile.exists());
        boolean flag = dataFile.createNewFile() && indexFile.createNewFile();
        Preconditions.checkState(flag);
    }

    static class GfsIndex {
        private int offset;
        private int len;

    }
}
